package org.putrpctorest.service.impl.copy;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.util.TraceClassVisitor;
import org.putrpctorest.container.Container;
import org.putrpctorest.resource.Method;
import org.putrpctorest.resource.Parameter;
import org.putrpctorest.resource.Resource;

public class ServiceClassGeneratorImpl {

    private Container container;

    public ServiceClassGeneratorImpl(Container container) {
        this.container = container;
    }

    private String className;
    private static List<String> methodNames = new ArrayList<String>();
    private static List<String> classNames = new ArrayList<String>();

    public Container getContainer() {
        return container;
    }

    public Class<?> generateClass(Container container) {
        // return generateClassForServiceDefinitions(container.getServices());
        return null;
    }

    // private Class<?> generateClassForServiceDefinitions(Services restServices) {
    // String className = getClassName(restServices);
    // ClassWriter restClassWriter1 = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES);
    //
    // TraceClassVisitor restClassWriter = new TraceClassVisitor(restClassWriter1, new PrintWriter(System.out));
    //
    // // CheckClassAdapter restClassWriter = new
    // // CheckClassAdapter(restClassWriter2);
    //
    // // CheckClassAdapter cc = new CheckClassAdapter(restClassWriter);
    // // ClassVisitor tv = new TraceClassVisitor(cc, );
    // // cc.verify(, true, new PrintWriter(System.out));
    //
    // restClassWriter.visit(V1_6, ACC_PUBLIC, "org/cxfextension/generatedclasses/" + className, null,
    // "java/lang/Object", null);
    //
    // AnnotationVisitor path = restClassWriter.visitAnnotation(Type.getDescriptor(Path.class), true);
    // path.visit("value", "/");
    // path.visitEnd();
    //
    // generateConstructor(restClassWriter);
    // for (Resource resource : restServices.getResources()) {
    // for (Method method : resource.getMethods()) {
    // generateMethod(restClassWriter, resource, method);
    // }
    // }
    // restClassWriter.visitEnd();
    //
    // return loadClass(restClassWriter1.toByteArray());
    // }

    private void generateConstructor(TraceClassVisitor restClassWriter) {
        MethodVisitor methodVisitor = restClassWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);

        methodVisitor.visitCode();
        methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V");
        methodVisitor.visitInsn(Opcodes.RETURN);
        methodVisitor.visitMaxs(0, 0);
        methodVisitor.visitEnd();

    }

    private void generateMethod(ClassVisitor restClassWriter, Resource resource, Method method) {
        int paramCount = method.getParameters().size();
        String methodName = getMethodName(resource, method);
        String methodSignature = getMethodSignature(resource, method);

        MethodVisitor methodVisitor = restClassWriter.visitMethod(ACC_PUBLIC, methodName, methodSignature, null,
                new String[] {"java/lang/Exception"});

        AnnotationVisitor get = methodVisitor.visitAnnotation(Type.getDescriptor(GET.class), true);
        get.visitEnd();

        AnnotationVisitor path = methodVisitor.visitAnnotation(Type.getDescriptor(Path.class), true);
        path.visit("value", resource.getPath());
        path.visitEnd();

        AnnotationVisitor produces = methodVisitor.visitAnnotation(Type.getDescriptor(Produces.class), true);
        AnnotationVisitor array = produces.visitArray("value");
        for (String producesStr : getProduces(resource, method)) {
            array.visit("", producesStr);
        }
        array.visitEnd();
        produces.visitEnd();

        AnnotationVisitor consumes = methodVisitor.visitAnnotation(Type.getDescriptor(Consumes.class), true);
        array = consumes.visitArray("value");
        for (String consumesStr : getConsumes(resource, method)) {
            array.visit("", consumesStr);
        }
        array.visitEnd();
        consumes.visitEnd();

        int i = 0;
        for (Parameter parameter : method.getParameters()) {
            Class<?> annotation = parameter.getType().getParameterAnnotation();

            AnnotationVisitor paramAnnotationVisitor = methodVisitor.visitParameterAnnotation(i,
                    Type.getDescriptor(annotation), true);
            if (annotation.isAssignableFrom(PathParam.class) || annotation.isAssignableFrom(QueryParam.class)
                    || annotation.isAssignableFrom(MatrixParam.class) || annotation.isAssignableFrom(HeaderParam.class)
                    || annotation.isAssignableFrom(CookieParam.class) || annotation.isAssignableFrom(FormParam.class)) {
                paramAnnotationVisitor.visit("value", parameter.getName());
            }
            paramAnnotationVisitor.visitEnd();
            i++;
        }

        methodVisitor.visitCode();
        methodVisitor.visitTypeInsn(Opcodes.NEW, Type.getInternalName(HashMap.class));
        methodVisitor.visitInsn(Opcodes.DUP);
        methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(HashMap.class), "<init>", "()V");
        methodVisitor.visitVarInsn(Opcodes.ASTORE, paramCount + 1);

        for (Parameter parameter : method.getParameters()) {
            methodVisitor.visitVarInsn(Opcodes.ALOAD, paramCount + 1);
            methodVisitor.visitLdcInsn(parameter.getName());
            methodVisitor.visitVarInsn(Opcodes.ALOAD, i + 1);
            methodVisitor.visitMethodInsn(
                    Opcodes.INVOKEINTERFACE,
                    "java/util/Map",
                    "put",
                    Type.getMethodDescriptor(Type.getType(Object.class),
                            new Type[] {Type.getType(Object.class), Type.getType(Object.class)}));
            methodVisitor.visitInsn(Opcodes.POP);
        }

        methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getDescriptor(ServiceInvokerImpl.class),
                "getInstance", Type.getMethodDescriptor(Type.getType(ServiceInvokerImpl.class), new Type[] {}));
        methodVisitor.visitVarInsn(Opcodes.ASTORE, paramCount + 2);
        methodVisitor.visitLdcInsn(getMethodGlobalId(method));
        methodVisitor.visitVarInsn(Opcodes.ASTORE, paramCount + 3);
        methodVisitor.visitVarInsn(Opcodes.ALOAD, paramCount + 2);
        methodVisitor.visitVarInsn(Opcodes.ALOAD, paramCount + 1);
        methodVisitor.visitVarInsn(Opcodes.ALOAD, paramCount + 3);
        methodVisitor.visitMethodInsn(
                Opcodes.INVOKEVIRTUAL,
                Type.getDescriptor(ServiceInvokerImpl.class),
                "invokeService",
                Type.getMethodDescriptor(Type.getType(Object.class),
                        new Type[] {Type.getType(Map.class), Type.getType(String.class)}));
        methodVisitor.visitInsn(Opcodes.ARETURN);
        methodVisitor.visitMaxs(0, 0);
        methodVisitor.visitEnd();

    }

    private String getMethodGlobalId(Method method) {
        // return method.getResource().getServicesDefinitions().getId() + ":" + method.getResource().getId() + ":"
        // + method.getId();
        return null;
    }

    private String getMethodSignature(Resource resource, Method method) {
        Type[] paramTypes = new Type[method.getParameters().size()];

        int i = 0;
        for (Parameter parameter : method.getParameters()) {
            if (parameter.getJavaType() != String.class) {
                throw new IllegalStateException("Only Strings are allowed as parameters..");
            }
            paramTypes[i] = Type.getType(parameter.getJavaType());
            i++;
        }
        return Type.getMethodDescriptor(Type.getType(Object.class), paramTypes);
    }

    public static String getMethodName(Resource resource, Method method) {
        String firstProposalForMethodName = getJavaIdentifierForName("service_method_" + resource.getId() + "_"
                + method.getId());
        StringBuffer proposedMethodName = new StringBuffer(firstProposalForMethodName);
        while (methodNames.contains(firstProposalForMethodName)) {
            proposedMethodName.append('_');
        }

        return proposedMethodName.toString();
    }

    public static String getClassName(
    // Services restServices
    ) {

        // String firstProposalForClassName = getJavaIdentifierForName("RestServiceClass" + restServices.getId());
        // StringBuffer proposedClassName = new StringBuffer(firstProposalForClassName);
        //
        // while (classNames.contains(proposedClassName.toString())) {
        // proposedClassName.append('_');
        // }
        // while (classAlreadyExists(proposedClassName.toString())) {
        // proposedClassName.append('_');
        // }
        // classNames.add(firstProposalForClassName.toString());
        // return proposedClassName.toString();
        return null;
    }

    private static String getJavaIdentifierForName(String name) {
        StringBuffer javaIdentifier = new StringBuffer();

        for (int i = 0; i < name.length(); i++) {
            if (Character.isJavaIdentifierPart(name.charAt(i))) {
                javaIdentifier.append(name.charAt(i));
            } else {
                javaIdentifier.append('_');
            }
        }

        return javaIdentifier.toString();

    }

    private static boolean classAlreadyExists(String className) {

        try {
            Class.forName(className);
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    private Class<?> loadClass(byte[] b) {
        // override classDefine (as it is protected) and define the class.
        Class<?> clazz = null;
        try {
            ClassLoader loader = ClassLoader.getSystemClassLoader();
            Class<?> cls = Class.forName("java.lang.ClassLoader");
            java.lang.reflect.Method method = cls.getDeclaredMethod("defineClass", new Class[] {String.class,
                    byte[].class, int.class, int.class});

            // protected method invocaton
            method.setAccessible(true);
            try {
                Object[] args = new Object[] {className, b, new Integer(0), new Integer(b.length)};
                clazz = (Class<?>) method.invoke(loader, args);
            } finally {
                method.setAccessible(false);
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
        return clazz;
    }

    private String[] getConsumes(Resource resource, Method method) {

        Set<String> allConsumes = new HashSet<String>();

        for (String resourceConsumes : resource.getConsumes()) {
            allConsumes.add(resourceConsumes);
        }

        for (String methodConsumes : method.getConsumes()) {
            allConsumes.add(methodConsumes);
        }

        return allConsumes.toArray(new String[0]);
    }

    private String[] getProduces(Resource resource, Method method) {

        Set<String> allProduces = new HashSet<String>();

        for (String resourceProduces : resource.getProduces()) {
            allProduces.add(resourceProduces);
        }

        for (String methodProduces : method.getProduces()) {
            allProduces.add(methodProduces);
        }

        return allProduces.toArray(new String[0]);
    }

}
